package com.jfirer.validator.validation;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.validation.ConstraintValidatorFactory;
import javax.validation.ConstraintViolation;
import javax.validation.ElementKind;
import javax.validation.ParameterNameProvider;
import javax.validation.executable.ExecutableValidator;
import javax.validation.metadata.ConstructorDescriptor;
import javax.validation.metadata.ExecutableDescriptor;
import javax.validation.metadata.MethodDescriptor;
import javax.validation.metadata.ParameterDescriptor;
import javax.validation.metadata.ReturnValueDescriptor;
import com.jfirer.validator.BeanValidator;
import com.jfirer.validator.ExecutableDescriptorFactory;
import com.jfirer.validator.WritableConstraintViolation;
import com.jfirer.validator.descriptor.ConstructorDescriptorImpl;
import com.jfirer.validator.descriptor.MethodDescriptorImpl;
import com.jfirer.validator.path.api.ConnectionNode;
import com.jfirer.validator.path.api.ConnectionNodeImpl;
import com.jfirer.validator.util.Commons;

public class ExecutableValidatorImpl implements ExecutableValidator, ExecutableDescriptorFactory
{
	private       ConstraintValidatorFactory                           constraintValidatorFactory;
	private final ConcurrentMap<Method, MethodDescriptor>              methodDescriptorStore		= new ConcurrentHashMap<Method, MethodDescriptor>();
	private final ConcurrentMap<Constructor<?>, ConstructorDescriptor> constructorDescriptorStore	= new ConcurrentHashMap<Constructor<?>, ConstructorDescriptor>();
	private final ParameterNameProvider                                parameterNameProvider;
	private final BeanValidator                                        beanValidator;
	
	public ExecutableValidatorImpl(ParameterNameProvider parameterNameProvider, BeanValidator beanValidator, ConstraintValidatorFactory constraintValidatorFactory)
	{
		this.parameterNameProvider = parameterNameProvider;
		this.beanValidator = beanValidator;
		this.constraintValidatorFactory = constraintValidatorFactory;
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public <T> Set<ConstraintViolation<T>> validateParameters(T object, Method method, Object[] parameterValues, Class<?>... groups)
	{
		Set<ConstraintViolation<T>> result           = new HashSet<ConstraintViolation<T>>();
		Set<Class<?>>               validateGroups   = Commons.defineValidateGroups(groups);
		ConnectionNode              methodNode       = new ConnectionNodeImpl().setElementKind(ElementKind.METHOD).setMethod(method);
		MethodDescriptor            methodDescriptor = getMethodDescriptor(method);
		validateExecutableParameters(parameterValues, methodDescriptor, result, validateGroups, methodNode);
		for (ConstraintViolation<T> each : result)
		{
			((WritableConstraintViolation<T>) each).setRootBean(object);
			((WritableConstraintViolation<T>) each).setRootBeanClass((Class<T>) object.getClass());
		}
		return result;
	}
	
	private <T> void validateExecutableParameters(Object[] parameterValues, ExecutableDescriptor executableDescriptor, Set<ConstraintViolation<T>> result, Set<Class<?>> validateGroups, ConnectionNode executableNode)
	{
		if (executableDescriptor.hasConstrainedParameters() == false)
		{
			return;
		}
		for (ParameterDescriptor parameterDescriptor : executableDescriptor.getParameterDescriptors())
		{
			ConnectionNode parameterNode = executableNode.next()//
			        .setParameterIndexAndName(parameterDescriptor.getIndex(), parameterDescriptor.getName())//
			        .setElementKind(ElementKind.PARAMETER);
			if (parameterDescriptor.hasConstraints())
			{
				beanValidator.validateConstraintedElement(result, validateGroups, parameterDescriptor.getConstraintDescriptors(), parameterNode, null, parameterValues[parameterDescriptor.getIndex()]);
			}
			if (parameterDescriptor.isCascaded() && parameterValues[parameterDescriptor.getIndex()] != null)
			{
				beanValidator.validateCascadedElement(result, validateGroups, parameterNode, parameterValues[parameterDescriptor.getIndex()]);
			}
		}
		if (executableDescriptor.hasConstrainedParameters())
		{
			ConnectionNode crossParameterNode = executableNode.next().setElementKind(ElementKind.CROSS_PARAMETER);
			beanValidator.validateConstraintedElement(result, validateGroups, executableDescriptor.getCrossParameterDescriptor().getConstraintDescriptors(), crossParameterNode, null, parameterValues);
		}
		
	}
	
	@Override
	public <T> Set<ConstraintViolation<T>> validateReturnValue(T object, Method method, Object returnValue, Class<?>... groups)
	{
		Set<ConstraintViolation<T>> result = new HashSet<ConstraintViolation<T>>();
		if (returnValue == null)
		{
			return result;
		}
		MethodDescriptor methodDescriptor = getMethodDescriptor(method);
		ConnectionNode methodNode = new ConnectionNodeImpl().setElementKind(ElementKind.METHOD).setMethod(method);
		Set<Class<?>> validateGroups = Commons.defineValidateGroups(groups);
		validateExecutableReturnValue(returnValue, result, methodDescriptor, methodNode, validateGroups);
		return result;
	}
	
	private <T> void validateExecutableReturnValue(Object returnValue, Set<ConstraintViolation<T>> result, ExecutableDescriptor executableDescriptor, ConnectionNode executableNode, Set<Class<?>> validateGroups)
	{
		if (executableDescriptor.hasConstrainedReturnValue() == false)
		{
			return;
		}
		ConnectionNode returnValueNode = executableNode.next().setElementKind(ElementKind.RETURN_VALUE);
		ReturnValueDescriptor returnValueDescriptor = executableDescriptor.getReturnValueDescriptor();
		if (returnValueDescriptor.hasConstraints())
		{
			beanValidator.validateConstraintedElement(result, validateGroups, returnValueDescriptor.getConstraintDescriptors(), returnValueNode, null, returnValue);
		}
		if (returnValueDescriptor.isCascaded())
		{
			beanValidator.validateCascadedElement(result, validateGroups, returnValueNode, returnValue);
		}
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public <T> Set<ConstraintViolation<T>> validateConstructorParameters(Constructor<? extends T> constructor, Object[] parameterValues, Class<?>... groups)
	{
		Set<ConstraintViolation<T>> result = new HashSet<ConstraintViolation<T>>();
		Set<Class<?>> validateGroups = Commons.defineValidateGroups(groups);
		ConnectionNode methodNode = new ConnectionNodeImpl().setElementKind(ElementKind.CONSTRUCTOR).setConstructor(constructor);
		ConstructorDescriptor constructorDescriptor = getConstructorDescriptor(constructor);
		validateExecutableParameters(parameterValues, constructorDescriptor, result, validateGroups, methodNode);
		for (ConstraintViolation<T> each : result)
		{
			((WritableConstraintViolation<T>) each).setRootBeanClass((Class<T>) constructor.getDeclaringClass());
		}
		return result;
	}
	
	@Override
	public <T> Set<ConstraintViolation<T>> validateConstructorReturnValue(Constructor<? extends T> constructor, T createdObject, Class<?>... groups)
	{
		Set<ConstraintViolation<T>> result = new HashSet<ConstraintViolation<T>>();
		if (createdObject == null)
		{
			return result;
		}
		ConstructorDescriptor constructorDescriptor = getConstructorDescriptor(constructor);
		ConnectionNode constructorNode = new ConnectionNodeImpl().setElementKind(ElementKind.CONSTRUCTOR).setConstructor(constructor);
		Set<Class<?>> validateGroups = Commons.defineValidateGroups(groups);
		validateExecutableReturnValue(createdObject, result, constructorDescriptor, constructorNode, validateGroups);
		return result;
	}
	
	@Override
	public final MethodDescriptor getMethodDescriptor(Method method)
	{
		MethodDescriptor methodDescriptor = methodDescriptorStore.get(method);
		if (methodDescriptor == null)
		{
			synchronized (method)
			{
				methodDescriptor = methodDescriptorStore.get(method);
				if (methodDescriptor == null)
				{
					methodDescriptor = new MethodDescriptorImpl(method, parameterNameProvider, constraintValidatorFactory);
				}
				methodDescriptorStore.put(method, methodDescriptor);
			}
		}
		return methodDescriptor;
	}
	
	@Override
	public final ConstructorDescriptor getConstructorDescriptor(Constructor<?> constructor)
	{
		ConstructorDescriptor constructorDescriptor = constructorDescriptorStore.get(constructor);
		if (constructorDescriptor == null)
		{
			synchronized (constructor)
			{
				constructorDescriptor = constructorDescriptorStore.get(constructor);
				if (constructorDescriptor == null)
				{
					constructorDescriptor = new ConstructorDescriptorImpl(constructor, parameterNameProvider, constraintValidatorFactory);
				}
				constructorDescriptorStore.put(constructor, constructorDescriptor);
			}
		}
		return constructorDescriptor;
	}
}
